#pragma once
#ifndef TENSORMATHELEMENTWISE_H_
#define TENSORMATHELEMENTWISE_H_

#include <math.h>

namespace cmpMat
{
    template <typename _T>
    class TensorDense;
    template <typename _T>
    class TensorBase;

    namespace {
        #pragma warning(disable:4244)
        enum BinaryElemOpEnum
        {
            Add,
            Subtract,
            Multiply,
            Divide
        };

        enum UnaryElemOpEnum
        {
            Pow2,
            Sin,
            Cos,
            Tan,
            Abs,
            lAbs,
            llAbs,
            fAbs,
            Acos,
            Asin,
            Atan,
            Cosh,
            Exp,
            Log,
            Log10,
            Sinh,
            Tanh,
            Sqrt
        };

        //double  __cdecl atan2(_In_ double _Y, _In_ double _X);
        //double  __cdecl fmod(_In_ double _X, _In_ double _Y);
        //double  __cdecl pow(_In_ double _X, _In_ double _Y);

        // recursively operate on tensor
        template <UnaryElemOpEnum _op, typename _T, typename _dimension_type, typename _Tensor>
        static void _UnaryElemOp(
            _Tensor& out,
            CmpMatPtr oldInd = 0,
            _dimension_type* dimP = nullptr)
        {
            _dimension_type dimR;
            if (dimP == nullptr)
            {
                dimR = _dimension_type(out.Dimensions().size(), 0);
                dimP = &dimR;
            }
            _dimension_type& dim = *dimP; 
            CmpMatPtr curInd = oldInd + 1;
            if (dim.size() == oldInd)
                out(dim) = UnOpCl<_op,_T>::Op(
                GetElem<_T>(out,dim));
            else
                for (int i = 0; i < out.size(oldInd); i++)
                {
                    dim[oldInd] = i;
                    _UnaryElemOp<_op, _T>(out, curInd, &dim);
                }
        }
        template <BinaryElemOpEnum _op, typename _T, typename _dimension_type, typename _Tensor, typename _T2>
        static void _BinaryElemOp(
            _Tensor& out,
            const _T2& b,
            CmpMatPtr oldInd = 0,
            _dimension_type* dimP = nullptr)
        {
            _dimension_type dimR;
            if (dimP == nullptr)
            {
                dimR = _dimension_type(out.Dimensions().size(), 0);
                dimP = &dimR;
            }
            _dimension_type& dim = *dimP; 
            CmpMatPtr curInd = oldInd + 1;
            if (dim.size() == oldInd)
                out(dim) = BinOpCl<_op,_T>::Op(
                GetElem<_T>(out,dim),
                GetElem<_T>(b,dim));
            else
                for (int i = 0; i < out.size(oldInd); i++)
                {
                    dim[oldInd] = i;
                    _BinaryElemOp<_op, _T>(out, b, curInd, &dim);
                }
        }

        template <typename _Tout, typename _dimension_type>
        const _Tout& GetElem (const _Tout& obj, const _dimension_type& dim)
        { return obj; }

        template <typename _Tout, typename _dimension_type>
        const _Tout& GetElem (const TensorBase<_Tout>& obj, const _dimension_type& dim)
        { return obj(dim); }

        template <typename _T> const _T& CnvI2Dbl(const _T& obj){ return obj; }
        double CnvI2Dbl(const short     int& obj) { return static_cast<double>(obj); }
        double CnvI2Dbl(const           int& obj) { return static_cast<double>(obj); }
        double CnvI2Dbl(const long      int& obj) { return static_cast<double>(obj); }
        double CnvI2Dbl(const long long int& obj) { return static_cast<double>(obj); }
        double CnvI2Dbl(const unsigned short     int& obj) { return static_cast<double>(obj); }
        double CnvI2Dbl(const unsigned           int& obj) { return static_cast<double>(obj); }
        double CnvI2Dbl(const unsigned long      int& obj) { return static_cast<double>(obj); }
        double CnvI2Dbl(const unsigned long long int& obj) { return static_cast<double>(obj); }

        template <UnaryElemOpEnum _op, typename _T>  struct UnOpCl {
            static _T Op(const _T& a);
        };
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Pow2, _T> { static _T Op(const _T& a)
        { return a * a; }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Sin, _T> { static _T Op(const _T& a)
        { return std::sin(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Cos, _T> { static _T Op(const _T& a)
        { return std::cos(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Tan, _T> { static _T Op(const _T& a)
        { return std::tan(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Abs, _T> { static _T Op(const _T& a)
        { return std::abs(a); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::lAbs, _T> { static _T Op(const _T& a)
        { return std::labs(a); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::llAbs, _T> { static _T Op(const _T& a)
        { return std::llabs(a); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::fAbs, _T> { static _T Op(const _T& a)
        { return std::fabs(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Acos, _T> { static _T Op(const _T& a)
        { return std::acos(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Asin, _T> { static _T Op(const _T& a)
        { return std::asin(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Atan, _T> { static _T Op(const _T& a)
        { return std::atan(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Cosh, _T> { static _T Op(const _T& a)
        { return std::cosh(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Exp, _T> { static _T Op(const _T& a)
        { return std::exp(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Log, _T> { static _T Op(const _T& a)
        { return std::log(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Log10, _T> { static _T Op(const _T& a)
        { return std::log10(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Sinh, _T> { static _T Op(const _T& a)
        { return std::sinh(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Tanh, _T> { static _T Op(const _T& a)
        { return std::tanh(CnvI2Dbl(a)); }};
        template <typename _T> struct UnOpCl<UnaryElemOpEnum::Sqrt, _T> { static _T Op(const _T& a)
        { return std::sqrt(CnvI2Dbl(a)); }};
        
        template <BinaryElemOpEnum _op, typename _T>  struct BinOpCl {
            static _T Op(const _T& a, const _T& b);
        };
        template <typename _T> struct BinOpCl<BinaryElemOpEnum::Add, _T>
        { static _T Op(const _T& a, const _T& b)
        { return a + b; }};
        template <typename _T> struct BinOpCl<BinaryElemOpEnum::Subtract, _T>
        { static _T Op(const _T& a, const _T& b)
        { return a - b; }};
        template <typename _T> struct BinOpCl<BinaryElemOpEnum::Multiply, _T>
        { static _T Op(const _T& a, const _T& b)
        { return a * b; }};
        template <typename _T> struct BinOpCl<BinaryElemOpEnum::Divide, _T>
        { static _T Op(const _T& a, const _T& b)
        { return a / b; }};

        template <UnaryElemOpEnum _op, template<typename> class _tensor, typename _T> _tensor<_T>& _ElemUnaryTo(_tensor<_T>& out)
        {
            _UnaryElemOp<_op, _T, _tensor<_T>::_dimension_type>(out);
            return out;
        }
        template <UnaryElemOpEnum _op, template<typename> class _tensorOut, template<typename> class _tensorIn, typename _T> 
        _tensorOut<_T> _ElemUnary(const _tensorIn<_T>& a)
        {
            _tensorOut<_T> out = _tensorOut<_T>::copyFrom(a);
            return _ElemUnaryTo<_op, _tensorOut, _T>(out);
        }

        template <BinaryElemOpEnum _op, typename _T, template<typename> class _tensorOut, template<typename> class _tensorIn>
        _tensorOut<_T>& _ElemBinaryTo(_tensorOut<_T>& out, const _tensorIn<_T>& other)
        {
            _BinaryElemOp<_op, _T, _tensorOut<_T>::_dimension_type>(out, other);
            return out;
        }
        template <BinaryElemOpEnum _op, typename _T, template<typename> class _tensorOut>
        _tensorOut<_T>& _ElemBinaryTo(_tensorOut<_T>& out, const _T& other)
        {
            _BinaryElemOp<_op, _T, _tensorOut<_T>::_dimension_type>(out, other);
            return out;
        }

        template <BinaryElemOpEnum _op, typename _T, template<typename> class _tensorOut, template<typename> class _tensorIn>
        _tensorOut<_T> _ElemBinary(const _tensorIn<_T>& a, const _T& b)
        {
            _tensorOut<_T> out = _tensorOut<_T>::copyFrom(a);
            return _ElemBinaryTo<_op, _T>(out, b); 
        }

        template <BinaryElemOpEnum _op, typename _T, template<typename> class _tensorOut, template<typename> class _tensorIn>
        _tensorOut<_T> _ElemBinary(const _T& a, const _tensorIn<_T>& b)
        {
            _tensorOut<_T> out = _tensorOut<_T>::copyFrom(b);
            return _ElemBinaryTo<_op, _T>(out, a); 
        }

        template <BinaryElemOpEnum _op, typename _T, template<typename> class _tensorOut, template<typename> class _tensorIn1, template<typename> class _tensorIn2>
        _tensorOut<_T> _ElemBinary(const _tensorIn1<_T>& a, const _tensorIn2<_T>& b)
        { 
            _tensorOut<_T> out = _tensorOut<_T>::copyFrom(a);
            return _ElemBinaryTo<_op, _T>(out, b); 
        }
#pragma warning(default:4018)
    }

    //template <typename _elementType>
    class TensorMathElementwise
    {
    private:
        // macro for binary functions
        #define BINARYFUNC(_name) \
            template <template<typename>class _tensor, typename _T, typename _Tin1, typename _Tin2>\
            static _tensor<_T> _name##(const _Tin1& a, const _Tin2& b)\
            { return _ElemBinary<BinaryElemOpEnum::##_name##, _T, _tensor>(a, b); }\
            template <template<typename>class _tensor, typename _T, typename _Tin> \
            static _tensor<_T>& _name##To(_tensor<_T>& thisM, const _Tin& other)\
            { return _ElemBinaryTo<BinaryElemOpEnum::##_name##, _T, _tensor>(thisM, other); }

        // macro for unary functions
        #define UNARYFUNC(_name) \
            template <template<typename>class _tensorOut, typename _T, template<typename>class _tensorIn> \
            static _tensorOut<_T> _name##(const _tensorIn<_T>& a)\
            { return _ElemUnary<UnaryElemOpEnum::##_name##, _tensorOut, _tensorIn, _T>(a); }\
            template <template<typename>class _tensor, typename _T> static _tensor<_T>& _name##To(_tensor<_T>& thisM)\
            { return _ElemUnaryTo<UnaryElemOpEnum::##_name##, _tensor, _T>(thisM); }

    public:
        // binary functions
        BINARYFUNC(Add);
        BINARYFUNC(Subtract);
        BINARYFUNC(Multiply);
        BINARYFUNC(Divide);
    
        // unary functions
        UNARYFUNC(Pow2);
        UNARYFUNC(Sin);
        UNARYFUNC(Cos);
        UNARYFUNC(Tan);
        UNARYFUNC(Abs);
        UNARYFUNC(lAbs);
        UNARYFUNC(llAbs);
        UNARYFUNC(fAbs);
        UNARYFUNC(Acos);
        UNARYFUNC(Asin);
        UNARYFUNC(Atan);
        UNARYFUNC(Cosh);
        UNARYFUNC(Exp);
        UNARYFUNC(Log);
        UNARYFUNC(Log10);
        UNARYFUNC(Sinh);
        UNARYFUNC(Tanh);
        UNARYFUNC(Sqrt);
    };

}
#endif